扫面一波,ArrayList(扩容机制+面试题+基础)结合通俗易懂 您所在的位置:网站首页 数组面试题 java 扫面一波,ArrayList(扩容机制+面试题+基础)结合通俗易懂

扫面一波,ArrayList(扩容机制+面试题+基础)结合通俗易懂

2023-07-26 04:38| 来源: 网络整理| 查看: 265

本文使用Java 8+

目录 前言第一章 集合与数组概念第二章 集合框架1、集合的两大类2、集合创建3、集合为何需要泛型 第三章 List接口1、List接口主要实现类2、迭代器2.1 为什么要迭代器2.2 什么是迭代器2.3 使用迭代器注意事项2.4 迭代器遍历的三种方法 3、ListIterator是什么 第四章 List接口常见面试题第五章 ArrayList源码剖析1、核心属性源码:2、构造方法(验证核心属性)3、ArrayList的扩容机制4、ArrayList扩容的核心方法。5、add方法(重点)当添加元素时容量长度小于默认容量的长度10时,源码解析当容量长度大于默认容量的长度10时,源码解析

前言 第一章 集合与数组概念

以往想要存储多个对象,我们想到用StringBuffered、数组来存储多个对象。但是在写程序的时候我们并不知道将需要多少个对象,或者是否需要更复杂的方式来存储对象,结果发展数组尺寸是固定的,这也太过于受限了吧,一点都不灵活,太难了,数组能开放点吗?

随后Java团队为了解决数组长度不可变,就提供了这么一个集合(Collection),其中基本的类型有 List 、 Set 、 Queue 和 Map。用Java集合类都可以自动地调整自己的大小。简直就要飞天了吧。因此,与数组不同是,在编程时,可以将任意数量的对象放置在集合中,而不用关心集合应该有多大。

小结:数组和集合区别

长度区别:数组长度固定;集合的长度可变(灵活易扩展)

存储内容区别:数组存储的是同一种类型的元素;集合可以存储不同类型的元素(但我们开发者考虑安全问题一般使用范型)

存储数据的类型区别:数组可以存储基本数据类型,也可以存储引用类型;集合只能存储引用类型(如存储int,它会自动装箱成Integer)

第二章 集合框架 1、集合的两大类

集合按照其存储结构可以分为两大类,分别是:

单列集合java.util.Collection,collection是单列集合类的根接口,它还有两个子接口分别是List和set接口。

List接口的特点:元素有序、元素可重复。(主要实现类有ArrayList、LinkedList、vector)Set接口的特点:元素无序,且不可重复。(主要实现类有HashSet、TreeSet、LinkedHashSet)

双列集合java.util.Map

集合本身是一个工具,它存放在java.util包中。

2、集合创建 List apples = new ArrayList(); List s = new ArrayList();

注意: ArrayList 已经被向上转型为了 List ,使用接口的目的是,如果想要改变具体实现,只需在创建时修改它就行了,就像下面这样:

List apples = new LinkedList();

注意:向上转型并非总是有效,因为某些具体类有额外的功能,就比如 LinkedList 具有 List 接口中未包含的额外方法,因此,就不能将它们向上转型为更通用的接口。

3、集合为何需要泛型

使用 Java 5 之前的集合的一个主要问题是编译器允许你向集合中插入不正确的类型,早期的Java使用Object来代表任意类型的,但是向下转型有强转的问题,这样就不太好了,程序不太安全。

Java泛型设计原则:在编译期防止将错误类型的对象放置到集合中,运行时期就不会出现ClassCastException异常。

细节:在 Java 7 之前,必须要在两端都进行类型声明

ArrayList apples = new ArrayList();

泛型小结:有了泛型,代码就会更加简洁不用强制转换、程序更加健壮、可读性和稳定性。

第三章 List接口

List 接口在 Collection 的基础上添加了许多方法,允许在 List 的中间插入和删除元素。

1、List接口主要实现类 ArrayList:擅长随机访问元素,但在 List 中间插入和删除元素时速度较慢。LinkedList:随机访问来说相对较慢,但它具有比 ArrayList 更大的特征集,它通过代价较低的在 List 中间进行的插入和删除操作,提供了优化的顺序访问。vector,老版本,这里就不多说了,现在都到Java14 2、迭代器 2.1 为什么要迭代器

我们都知道集合无非就是存储和获取的这样一个过程。毕竟,保存事物是集合最基本的工作,就比如List,插入是add()获取是get(),那么会发现要使用集合,必须对集合的确切类型编程。突然有一天,如果原本是对 List 编码的,但是后来发现如果能够将相同的代码应用于 Set 会更方便,此时应该怎么做?想到可能编写一段通用代码,但是它是用于不同类型的集合,那么如何才能不重写代码就可以应用于不同类型的集合?这时就使用迭代器。

2.2 什么是迭代器

迭代器(也是一种设计模式)的概念实现了这种抽象。迭代器是一个对象,它的工作就是遍历并选择序列中的每个对象,也就是提供了一种访问容器对象中的各个元素,并遍历出来。程序员不需要关心容器底层结构,就可以完美对容器进行遍历。

2.3 使用迭代器注意事项

(1)使用 iterator() 方法要求集合返回一个 Iterator。 Iterator 将准备好返回序列中的第一个元素。

(2)使用 next() 方法获得序列中的下一个元素。

(3)使用 hasNext() 方法检查序列中是否还有元素。

(4)使用 remove() 方法将迭代器最近返回的那个元素删除。

2.4 迭代器遍历的三种方法 List list = new ArrayList(); list.add("1、kobe"); list.add("2、詹姆斯"); list.add("3、字母哥"); list.add("4、浓眉"); list.add("5、乔丹");

第一种:for循环遍历(推荐)

for (Iterator it = list.iterator();it.hasNext();) { System.out.println(it.next()); }

第二种:声明周期就在大括号内(高并发推荐)

List list = new ArrayList(); list.add("1、kobe"); list.add("2、詹姆斯"); { Iterator iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } }

第三种:通过迭代器遍历

Iterator iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } 3、ListIterator是什么

ListIterator是继承自Iterator

ListIterator 是一个更强大的 Iterator 子类型,它只能由各种 List 类生成。 Iterator 只能向前移动,而 ListIterator 可以双向移动。

ListIterator迭代器包含的方法有:

add(E e): 将指定的元素插入列表,插入位置为迭代器当前位置之前hasNext():以正向遍历列表hasPrevious():如果以逆向遍历列表,列表迭代器前面还有元素,则返回 true,否则返回falsenext():返回列表中ListIterator指向位置后面的元素nextIndex():返回列表中ListIterator所需位置后面元素的索引set(E e):从列表中将next()或previous()返回的最后一个元素返回的最后一个元素更改为指定元素eprevious():返回列表中ListIterator指向位置前面的元素previousIndex():返回列表中ListIterator所需位置前面元素的索引

代码演示:

List list = new ArrayList(); list.add("kobe"); list.add("詹姆斯"); list.add("字母哥"); list.add("浓眉"); list.add("乔丹"); ListIterator listIterator = list.listIterator(); while (listIterator.hasNext()) { System.out.println(listIterator.next() + "," + listIterator.nextIndex()); } 最终输出: kobe, 1 詹姆斯,2 字母哥,3 浓眉, 4 乔丹, 5 第四章 List接口常见面试题

说一下List、Set、Map三者的区别?

* List是有序可重复 * Set是无序不可重复 * Map它是采用键值对来存储,两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

Arraylist 与 LinkedList 区别?

1. Arraylist底层使用的是数组;LinkedList底层使用的是双向链表,数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。(面试官有可能让你聊聊什么单链表和双链表) 2. Arraylist擅长随机访问元素,快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。LinkedList`随机访问来说相对较慢,但它具有比ArrayList更大的特征集。 3. ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。比如:执行add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 4. LinkedList采用链表存储,所以对于`add(E e)`方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置`i`插入和删除元素的话((add(int index, E element)`) 时间复杂度近似为`o(n))`因为需要先移动到指定位置再插入。

补充单链表、双链表、双向循环链表

单链表:一个节点指向下一个节点(操作链表要时刻记住的是:节点中指针域指向的就是一个节点了!) 双向链表:包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。 双向循环链表:最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。

推荐阅读:看图轻松理解数据结构与算法系列(双向链表)

说一下ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?

Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 Arraylist不是同步的,所以在不需要保证线程安全时建议使用Arraylist。

ArrayList的大小是如何自动增加的?

1、添加元素时,首先进行判断是否大于默认容量10 2、如果,小于默认容量,直接在原来基础上+1,元素添加完毕 3、如果,大于默认容量,则需要进行扩容,扩容核心是grow()方法 3.1 扩容之前,首先创建一个新的数组,且旧数组被复制到新的数组中 这样就得到了一个全新的副本,我们在操作时就不会影响原来数组了 3.2 然后通过位运算符将新的容量更新为旧容量的1.5陪(原来长度的一半再加上原长度也就是每次扩容是原来的1.5倍) 3.3 如果新的容量-旧的容量}; //保存ArrayList数据的数组 transient Object[] elementData; //该属性设置容量大小,ArrayList 所包含的元素个数 private int size; 2、构造方法(验证核心属性) //1、构造具有指定初始容量的空列表 public ArrayList(int initialCapacity) { //如果指定容量大于0,那么数组就进行初始化成对应的容量 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //如果初始容量为0,它会默认给定一个空的数组(EMPTY_ELEMENTDATA) this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } //2、构造一个初始容量为10的空列表。 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //3、按照集合的迭代器返回的顺序构造一个包含指定集合元素的列表。 public ArrayList(Collection c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // defend against c.toArray (incorrectly) not returning Object[] // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } } 3、ArrayList的扩容机制 //在使用ensureCapacity操作添加大量元素之前,应用程序可以增加ArrayList实例的容量。 //这可能会减少增量重新分配的数量。 public void ensureCapacity(int minCapacity) { if (minCapacity > elementData.length && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA && minCapacity // overflow-conscious code int oldCapacity = elementData.length; //将新容量更新为旧容量的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity if (minCapacity MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } //返回此列表中的元素数。 public int size() { return size; } 5、add方法(重点)

具体实现步骤分为:首先判断是否需要扩容、再插入元素

当添加元素时容量长度小于默认容量的长度10时,源码解析 //1、添加元素,e获取你添加的元素 public boolean add(E e) { modCount++;//在之前的元素自增 add(e, elementData, size); return true; } //2、判断是否需要扩容 private void add(E e, Object[] elementData, int s) { //判断s长度是否等于默认容量长度10 //如果大于默认容量10,则走扩容elementData = grow(); //如果小于默认容量10,则走直接添加elementData[s] = e; if (s == elementData.length) elementData = grow(); elementData[s] = e; //小于默认容量长度10,则size等于原来的+1 size = s + 1; } //3、最终返回size = s + 1; public boolean add(E e) { modCount++; add(e, elementData, size); //最终的返回 return true; } 当容量长度大于默认容量的长度10时,源码解析 //1、添加元素 public boolean add(E e) { modCount++; add(e, elementData, size); return true; } //2、判断是否需要扩容 private void add(E e, Object[] elementData, int s) { //判断s长度是否等于默认容量长度10 //如果大于默认容量10,则走扩容elementData = grow(); //如果小于默认容量10,则走直接添加elementData[s] = e; if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; } //3、默认容量+1 private Object[] grow() { return grow(size + 1); } //4、minCapacity=默认容量size+1 private Object[] grow(int minCapacity) { //要复制的数组;拷贝的新数组长度 return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));//进行扩容了 } //5、实现扩容 //用来确定是否需要容量,最后调用grow()来扩容 //假设当添加11个元素时,minCapacity=11,而默认数组为10,此时需要进行扩容 private int newCapacity(int minCapacity) { // overflow-conscious code //5.1 旧容量10 //扩容1.5倍 int oldCapacity = elementData.length; //5.2新的容量=旧容量+(oldCapacity >> 1) int newCapacity = oldCapacity + (oldCapacity >> 1); //5.3判断,如果新的容量-最小容量 return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity)); } //7、newLength扩容后的长度、original原始的 @SuppressWarnings("unchecked") public static T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); } //8、数组的拷贝复制 public static T[] copyOf(U[] original, int newLength, Class newType) { @SuppressWarnings("unchecked") T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); //Math.min进行计算 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } //9、a=默认容量10,b=扩容后的容量 public static int min(int a, int b) { return (a return (T[]) copyOf(original, newLength, original.getClass()); } //12、grow()方法扩容得到最终的长度 private Object[] grow() { return grow(size + 1); } //13、 private void add(E e, Object[] elementData, int s) { if (s == elementData.length) //13.1 elementData = grow(); elementData = grow(); //13.2 e是存入的元素 elementData[s] = e; //13.3最终长度为size = s + 1; size = s + 1; } //14、结束 public boolean add(E e) { modCount++; add(e, elementData, size); //返回 return true; }

小结:

1、添加元素时,首先进行判断是否大于默认容量10 2、如果,小于默认容量,直接在原来基础上+1,元素添加完毕 3、如果,大于默认容量,则需要进行扩容,扩容核心是grow()方法 3.1 扩容之前,首先创建一个新的数组,且旧数组被复制到新的数组中 这样就得到了一个全新的副本,我们在操作时就不会影响原来数组了 3.2 然后通过位运算符将新的容量更新为旧容量的1.5陪 3.3 如果新的容量-旧的容量


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有